/*
 * Copyright 2009 Red Hat, Inc.
 *
 * Red Hat licenses this file to you under the Apache License, version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.ipfilter;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * This class allows to check if an IP-V4-Address is contained in a subnet.<BR>
 * Supported Formats for the Subnets are: 1.1.1.1/255.255.255.255 or 1.1.1.1/32 (CIDR-Notation)
 * and (InetAddress,Mask) where Mask is a integer for CIDR-notation or a String for Standard Mask notation.<BR>
 * <BR><BR>Example1:<BR>
 * <tt>IpV4Subnet ips = new IpV4Subnet("192.168.1.0/24");</tt><BR>
 * <tt>System.out.println("Result: "+ ips.contains("192.168.1.123"));</tt><BR>
 * <tt>System.out.println("Result: "+ ips.contains(inetAddress2));</tt><BR>
 * <BR>Example1 bis:<BR>
 * <tt>IpV4Subnet ips = new IpV4Subnet(inetAddress, 24);</tt><BR>
 * where inetAddress is 192.168.1.0 and inetAddress2 is 192.168.1.123<BR>
 * <BR><BR>Example2:<BR>
 * <tt>IpV4Subnet ips = new IpV4Subnet("192.168.1.0/255.255.255.0");</tt><BR>
 * <tt>System.out.println("Result: "+ ips.contains("192.168.1.123"));</tt><BR>
 * <tt>System.out.println("Result: "+ ips.contains(inetAddress2));</tt><BR>
 * <BR>Example2 bis:<BR>
 * <tt>IpV4Subnet ips = new IpV4Subnet(inetAddress, "255.255.255.0");</tt><BR>
 * where inetAddress is 192.168.1.0 and inetAddress2 is 192.168.1.123<BR>

 * @author frederic bregier
 *
 */
public class IpV4Subnet implements IpSet, Comparable<IpV4Subnet>
{
   private static final int SUBNET_MASK = 0x80000000;

   private static final int BYTE_ADDRESS_MASK = 0xFF;

   private InetAddress inetAddress;

   private int subnet;

   private int mask;

   private int cidrMask;

   /**
    * Create IpV4Subnet for ALL (used for ALLOW or DENY ALL)
    */
   public IpV4Subnet()
   {
      // ALLOW or DENY ALL
      mask = -1;
      // other will be ignored
      inetAddress = null;
      subnet = 0;
      cidrMask = 0;
   }

   /**
    * Create IpV4Subnet using the CIDR or normal Notation<BR>
    * i.e.:
    * IpV4Subnet subnet = new IpV4Subnet("10.10.10.0/24"); or
    * IpV4Subnet subnet = new IpV4Subnet("10.10.10.0/255.255.255.0");
    * @param netAddress a network address as string.
    * @throws UnknownHostException
    * */
   public IpV4Subnet(String netAddress) throws UnknownHostException
   {
      setNetAddress(netAddress);
   }

   /**
    * Create IpV4Subnet using the CIDR Notation
    * @param inetAddress
    * @param cidrNetMask
    */
   public IpV4Subnet(InetAddress inetAddress, int cidrNetMask)
   {
      setNetAddress(inetAddress, cidrNetMask);
   }

   /**
    * Create IpV4Subnet using the normal Notation
    * @param inetAddress
    * @param netMask
    */
   public IpV4Subnet(InetAddress inetAddress, String netMask)
   {
      setNetAddress(inetAddress, netMask);
   }

   /**
    * Sets the Network Address in either CIDR or Decimal Notation.<BR>
    * i.e.: setNetAddress("1.1.1.1/24"); or<BR>
    * setNetAddress("1.1.1.1/255.255.255.0");<BR>
    * @param netAddress a network address as string.
    * @throws UnknownHostException
    * */
   private void setNetAddress(String netAddress) throws UnknownHostException
   {
      Vector<Object> vec = new Vector<Object>();
      StringTokenizer st = new StringTokenizer(netAddress, "/");
      while (st.hasMoreTokens())
      {
         vec.add(st.nextElement());
      }

      if (vec.get(1).toString().length() < 3)
      {
         setNetId(vec.get(0).toString());
         setCidrNetMask(Integer.parseInt(vec.get(1).toString()));
      }
      else
      {
         setNetId(vec.get(0).toString());
         setNetMask(vec.get(1).toString());
      }
   }

   /**
    * Sets the Network Address in CIDR Notation.
    * @param inetAddress
    * @param cidrNetMask
    * */
   private void setNetAddress(InetAddress inetAddress, int cidrNetMask)
   {
      setNetId(inetAddress);
      setCidrNetMask(cidrNetMask);
   }

   /**
    * Sets the Network Address in Decimal Notation.
    * @param inetAddress
    * @param netMask
    * */
   private void setNetAddress(InetAddress inetAddress, String netMask)
   {
      setNetId(inetAddress);
      setNetMask(netMask);
   }

   /**
    * Sets the BaseAdress of the Subnet.<BR>
    * i.e.: setNetId("192.168.1.0");
    * @param netId a network ID
    * @throws UnknownHostException
    * */
   private void setNetId(String netId) throws UnknownHostException
   {
      InetAddress inetAddress1 = InetAddress.getByName(netId);
      this.setNetId(inetAddress1);
   }

   /**
    * Compute integer representation of InetAddress
    * @param inetAddress1
    * @return the integer representation
    */
   private int toInt(InetAddress inetAddress1)
   {
      byte[] address = inetAddress1.getAddress();
      int net = 0;
      for (byte addres : address)
      {
         net <<= 8;
         net |= addres & BYTE_ADDRESS_MASK;
      }
      return net;
   }

   /**
    * Sets the BaseAdress of the Subnet.
    * @param inetAddress
    * */
   private void setNetId(InetAddress inetAddress)
   {
      this.inetAddress = inetAddress;
      subnet = toInt(inetAddress);
   }

   /**
    * Sets the Subnet's Netmask in Decimal format.<BR>
    * i.e.: setNetMask("255.255.255.0");
    * @param netMask a network mask
    * */
   private void setNetMask(String netMask)
   {
      StringTokenizer nm = new StringTokenizer(netMask, ".");
      int i = 0;
      int[] netmask = new int[4];
      while (nm.hasMoreTokens())
      {
         netmask[i] = Integer.parseInt(nm.nextToken());
         i++;
      }
      int mask1 = 0;
      for (i = 0; i < 4; i++)
      {
         mask1 += Integer.bitCount(netmask[i]);
      }
      setCidrNetMask(mask1);
   }

   /**
    * Sets the CIDR Netmask<BR>
    * i.e.: setCidrNetMask(24);
    * @param cidrNetMask a netmask in CIDR notation
    * */
   private void setCidrNetMask(int cidrNetMask)
   {
      cidrMask = cidrNetMask;
      mask = SUBNET_MASK >> cidrMask - 1;
   }

   /**
    * Compares the given IP-Address against the Subnet and returns true if
    * the ip is in the subnet-ip-range and false if not.
    * @param ipAddr an ipaddress
    * @return returns true if the given IP address is inside the currently
    * set network.
    * @throws UnknownHostException
    * */
   public boolean contains(String ipAddr) throws UnknownHostException
   {
      InetAddress inetAddress1 = InetAddress.getByName(ipAddr);
      return this.contains(inetAddress1);
   }

   /**
    * Compares the given InetAddress against the Subnet and returns true if
    * the ip is in the subnet-ip-range and false if not.
    * @param inetAddress1
    * @return returns true if the given IP address is inside the currently
    * set network.
    * */
   public boolean contains(InetAddress inetAddress1)
   {
      if (mask == -1)
      {
         // ANY
         return true;
      }
      return (toInt(inetAddress1) & mask) == subnet;
   }

   @Override
   public String toString()
   {
      return inetAddress.getHostAddress() + "/" + cidrMask;
   }

   @Override
   public boolean equals(Object o)
   {
      if (!(o instanceof IpV4Subnet))
      {
         return false;
      }
      IpV4Subnet ipV4Subnet = (IpV4Subnet) o;
      return ipV4Subnet.subnet == subnet && ipV4Subnet.cidrMask == cidrMask;
   }

   /**
    * Compare two IpV4Subnet
    */
   public int compareTo(IpV4Subnet o)
   {
      if (o.subnet == subnet && o.cidrMask == cidrMask)
      {
         return 0;
      }
      if (o.subnet < subnet)
      {
         return 1;
      }
      else if (o.subnet > subnet)
      {
         return -1;
      }
      else if (o.cidrMask < cidrMask)
      {
         // greater Mask means less IpAddresses so -1
         return -1;
      }
      return 1;
   }

   /**
    * Simple test functions
    * @param args
    *          where args[0] is the netmask (standard or CIDR notation) and optional args[1] is
    *          the inetAddress to test with this IpV4Subnet
    */
   public static void main(String[] args)
   {
      if (args.length != 0)
      {
         IpV4Subnet ipV4Subnet = null;
         try
         {
            ipV4Subnet = new IpV4Subnet(args[0]);
         }
         catch (UnknownHostException e)
         {
            return;
         }
         if (args.length > 1)
         {
            try
            {
               System.out.println("Is IN: " + args[1] + " " + ipV4Subnet.contains(args[1]));
            }
            catch (UnknownHostException e)
            {
            }
         }
      }
   }
}
